Implementazione dell'assegnazione del registro ingenuo per macchine x86

0

Sto scrivendo un targeting per compilatore giocattolo su x86-64 di macchine. Ma mi trovo di fronte a diversi problemi nell'implementare l'allocazione dei registri con la rappresentazione intermedia lineare. Uso NASM e GCC per generare file eseguibili.

Nella rappresentazione intermedia del codice di origine, assumiamo che ci siano registri infiniti (la memoria è considerata una sorta di registro virtuale). Li ho divisi in due tipi:

  • Registri singoli: questo tipo di registro virtuale rappresenta una variabile tipica.

  • Registri di offset: questo tipo di registro virtuale viene utilizzato per rappresentare l'accesso a un elemento di una matrice o di un membro di una struttura. Ad esempio, quando si traducono istruzioni come a.x = 3 e b[i] = 3 in rappresentazione intermedia, possiamo rappresentarlo come mov [$a + 0], 3 e mov [$b + $i * 4], 3 ( 4 è la dimensione di un intero), dove $a , $b e $i sono registri singoli e [$a + 0] e [$b + $i * 4] sono registri di offset.

Tuttavia, in x86-64 macchina, l'accesso alla memoria può essere rappresentato solo come [$reg1 + $reg2 * imm1 + imm2] , dove $reg1 e $reg2 sono registri fisici e imm1 e imm2 sono numeri immediati. Non riesco a capire come gli algoritmi di allocazione del registro si occupino del caso in cui l'algoritmo segni $va e $vb come nodo spillato con istruzione mov [$va + $vb * 4], 3 . In altre parole, $va e $vb devono essere un registro fisico piuttosto che un accesso alla memoria, ma se un registro virtuale è contrassegnato come nodo spillato, sarà considerato come accesso alla memoria allo stack frame.

Ad esempio, ottengo il seguente codice simile a C di origine:

int main() {
    int [] a = new int[10];
    int i = 3;
    a[i] = 4;
}

E lo traduco nella seguente rappresentazione intermedia:

call malloc, 10
mov $a, $retval       ; $retval is the return value of malloc
mov $i, 3
mov [$a + $i * 4], 4

Tuttavia, dopo aver assegnato i registri, trovo che il codice finale diventa:

push rbp
mov rbp, rsp
sub rsp, 16
mov rdi, 10
call malloc
mov qword [rbp-8], rax
mov qword [rbp-16], 3
mov [qword [rbp-8] + qword [rbp-16] * 4], 3  <- Compliation error occurs here

Mi chiedo se esiste una buona implementazione per risolvere questo problema. Grazie in anticipo.

UPD: Basile-Starynkevitch mi ha fornito un documento che mostra come GCC risolve questo problema. E sto cercando alcuni (probabilmente) metodi più semplici.

    
posta Kipsora Lawrence 27.05.2017 - 05:50
fonte

2 risposte

1

Il problema principale del tuo approccio è che abbassando il tuo codice al livello delle istruzioni di assemblaggio prima hai assegnato i valori ai registri, perdi la possibilità di variare l'assembly che devi produrre su la base di dove un valore deve venire. Se, tuttavia, hai definito una rappresentazione più astratta che potrebbe poi essere tradotta in linguaggio assembly dopo l'allocazione del registro, ciò faciliterebbe le cose.

Considera un linguaggio di calcolo dell'espressione giocattolo e l'espressione a + 2*b + c[a+d] . Questo potrebbe essere ridotto a una rappresentazione intermedia simile a questa:

v1 <= int32[a]       ; meaning: load the 32-bit int value at address "a"
v2 <= int32[b]
v3 <= 2*v2  (~v2)    ; (~nnn) means we no longer need to track this value. we could
                     ; determine this automatically, but for simplicity it's
                     ; easier to have it defined statically
v4 <= v1 + v3 (~v1,v3)
v5 <= int32[a]
v6 <= int32[d]
v7 <= v5 + v6 (~v5,v6)
v8 <= int32[c + 4*v7] (~v7)
v9 <= v4 + v8 (~v4,v8)

Usando un allocatore di registri molto semplice (e in qualche modo ingenuo) che può solo allocare due registri, EAX ed EBX (una restrizione messa in atto in modo da poter mostrare cosa succede quando si esauriscono i registri), si può iniziare annotando le posizioni di ciascun valore live:

v1 <= int32[a]              [EAX=v1, EBX=-]
v2 <= int32[b]              [EAX=v1, EBX=v2]
v3 <= 2*v2  (~v2)           [EAX=v1, EBX=v3]
v4 <= v1 + v3 (~v1,v3)      [EAX=v4, EBX=-]
v5 <= int32[a]              [EAX=v4, EBX=v5]
v6 <= int32[d]

... OK, quindi non abbiamo un registro disponibile qui. Quindi abbiamo bisogno di memorizzare temporaneamente un valore. Poiché questo allocatore di registri è irrimediabilmente ingenuo, non si rende conto che esistono modi migliori rispetto alla memorizzazione di un valore proveniente direttamente dalla memoria in una posizione di memoria temporanea. Seleziona solo il valore che non sarà necessario per il più lungo e aggiunge un'istruzione extra che lo memorizza nella memoria temporanea:

t1 <= v4                     [EAX=-, EBX=v5, t1=v4] 
v6 <= int32[d]               [EAX=v6, EBX=v5, t1=v4]
v7 <= v5 + v6 (~v5,v6)       [EAX=v7, EBX=-, t1=v4]
v8 <= int32[c + 4*v7] (~v7)  [EAX=v8, EBX=-, t1=v4]
v9 <= v4 + v8 (~v4,v8)

Quindi ora ha bisogno di usare un valore dalla memoria temporanea, quindi inserisce un'altra istruzione per reinserirla:

v4 <= t1                     [EAX=v8, EBX=v4, t1=v4]
v9 <= v4 + v8 (~v4,v8)       [EAX=v9]

Quindi ora i registri sono allocati, traduciamo il codice intermedio in linguaggio assembly:

; local variables: a = [ebp+4], b=[ebp+8], c=[ebp+12], d=[ebp+40]
; temporary storage available at [ebp-8]
;v1 <= int32[a]              [EAX=v1, EBX=-]
mov eax, dword [ebp+4]
;v2 <= int32[b]              [EAX=v1, EBX=v2]
mov ebx, dword [ebp+8]
;v3 <= 2*v2  (~v2)           [EAX=v1, EBX=v3]
shl ebx, 1
;v4 <= v1 + v3 (~v1,v3)      [EAX=v4, EBX=-]
add eax, ebx
;v5 <= int32[a]              [EAX=v4, EBX=v5]
mov ebx, dword [ebp + 4]
;t1 <= v4                     [EAX=-, EBX=v5, t1=v4] 
mov dword [ebp-8], eax    
;v6 <= int32[d]               [EAX=v6, EBX=v5, t1=v4]
mov eax, dword [ebp + 40]
;v7 <= v5 + v6 (~v5,v6)       [EAX=v7, EBX=-, t1=v4]
add eax, ebx
;v8 <= int32[c + 4*v7] (~v7)  [EAX=v8, EBX=-, t1=v4]
mov eax, dword [ebp + eax*4 + 12] 
;v4 <= t1                     [EAX=v8, EBX=v4, t1=v4]
mov ebx, dword [ebp-8]
;v9 <= v4 + v8 (~v4,v8)       [EAX=v9]
add eax, ebx

L'unica domanda rimanente è cosa succederebbe se l'operazione per il calcolo di v8 fosse invece:

v8 <= int32[c + 33*v7 + 4] (~v7)  [EAX=v8, EBX=-, t1=v4]

Quindi, avremmo bisogno di generare una serie di istruzioni, ma non sarebbe un problema. Sappiamo che EAX è disponibile, perché è stato sovraccaricato dal risultato di questo calcolo, quindi possiamo utilizzarlo come memoria temporanea senza bisogno di passare attraverso l'allocazione:

imul eax, 33
mov eax, dword [ebp + eax + 16]
    
risposta data 01.06.2017 - 02:27
fonte
2

Probabilmente non otterrai una risposta dettagliata, perché il tuo approccio è difettoso. È necessario un intero libro per spiegarlo.

Quindi leggi prima l'ultima edizione di Dragon Book . Ha diversi capitoli relativi alle tue preoccupazioni.

Leggi anche il wikipage su allocazione del registro e su Forma A-normale e modulo SSA . GCC ha molti documenti e diapositive sull'assegnazione dei registri, ad es. su Graph Coloring RA e su LRA

Il tuo compilatore dovrebbe essere composto da diversi passaggi. Ad esempio GCC ha più di trecento passaggi.

Ogni pass dovrebbe produrre una rappresentazione interna valida e spesso decorare la rappresentazione interna con annotazioni o trasformarla in un'altra.

È necessario assicurarsi che le proprie rappresentazioni interne siano coerenti con i passaggi di allocazione del registro. Suppongo che tu debba aggiungere passaggi aggiuntivi e più rappresentazioni interne (compreso un maggior numero di versamenti).

Si noti che l'allocazione dei registri è un argomento molto difficile. Per GCC, puoi trovare alcune persone che lavorano su di esso a tempo pieno per decine di anni.

Credo che i tuoi registri di offset siano un concetto imperfetto (e così anche i tuoi registri singoli ).

I registri sono posizioni e dovrebbero contenere valori (non le variabili di origine). Ad esempio, t[s.f + k] = t[s.f + k] + 2; (dove s è una struttura con un campo f e k è una variabile intera) dovrebbe essere compilato per mettere s.f + k in un registro

    
risposta data 27.05.2017 - 06:16
fonte

Leggi altre domande sui tag